package org.chorus;

import java.awt.geom.Rectangle2D;
import java.util.List;

import org.gicentre.utils.colour.ColourTable;

import jwo.landserf.gui.GISFrame;
import jwo.landserf.gui.SimpleGISFrame;
import jwo.landserf.process.SurfParam;
import jwo.landserf.structure.Footprint;
import jwo.landserf.structure.GISVector;
import jwo.landserf.structure.RasterMap;
import jwo.landserf.structure.VectorMap;
import processing.core.PApplet;
import processing.core.PConstants;
import processing.core.PFont;
import processing.core.PGraphics;
import processing.core.PImage;

//*********************************************************************************************
/** Collection of static utilities for displaying data. Includes raster image generators, 
 *  contour threading etc.
 *  @author Jo Wood, giCentre.
 *  @version 1.3.2, 13th March, 2013.
 */ 
// *********************************************************************************************

/* This file is part of the Chorus river bed analysis software package. Chorus is free software:
 * you can redistribute it and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * Chorus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this
 * source code (see COPYING included with this source code). If not, see 
 * http://www.gnu.org/licenses/.
 */

public class DisplayUtils 
{
	// --------------------------------------- Class variables ---------------------------------------
	
	/** Fonts used for text. */
	static PFont largeFont,mediumFont,smallFont;

	/** Layout types. */
	enum LayoutType {GRID, ROW, COLUMN}

	// ---------------------------------------- Static methods ---------------------------------------
	
	static ColourTable getCategoryPalette()
	{
		ColourTable cTable = new ColourTable();
		cTable.addDiscreteColourRule(0,228,26,28);
		cTable.addDiscreteColourRule(1,55,126,184);
		cTable.addDiscreteColourRule(2,77,175,74);
		cTable.addDiscreteColourRule(3,152,78,163);
		cTable.addDiscreteColourRule(4,255,127,0);
		cTable.addDiscreteColourRule(5,200,181,20);
		cTable.addDiscreteColourRule(6,166,86,40);
		cTable.addDiscreteColourRule(7,247,129,191);
		cTable.addDiscreteColourRule(8,153,153,153);
		cTable.addDiscreteColourRule(9,190,174,212);
		return cTable;
	}

	/** Sets up the fonts used for text display. This method must be called before any modules that use
	 *  text display are drawn.
	 *  @param parent Parent sketch capable of loading font files.
	 */
	static void setupFonts(PApplet parent)
	{
		//DisplayUtils.smallFont  = parent.loadFont("CartoGothicStd-Book-10.vlw");
		//DisplayUtils.mediumFont = parent.loadFont("CartoGothicStd-Book-14.vlw");
		//DisplayUtils.largeFont  = parent.loadFont("CartoGothicStd-Book-32.vlw");
		smallFont  = parent.createFont("GeosansLight.otf",10);
		mediumFont = parent.createFont("GeosansLight.otf",14);
		largeFont  = parent.createFont("GeosansLight.otf",32);
	}

	/** Creates an image representation of the given LandSerf raster. Uses the raster's own colour
	 *  table and shaded relief settings to generate the image pixel colours.
	 *  @param parent Sketch used to generate display.
	 *  @param raster Raster used to create image.
	 *  @param param Surface parameter to display. Should be selected from RasterMap's parameters (e.g. ELEV, SLOPE, ASPECT etc.).
	 *  @return Image representation of the raster.
	 */
	static PImage createImage(PApplet parent, RasterMap raster, int param)
	{
		// Standardize parameter scale and shading properties of all rasters.
		int origWindowSize = raster.getWSize();
		float origpShade = raster.getpShade();
		raster.setWSize(3);	
		raster.setpShade(0.3f);

		PImage img = parent.createImage(raster.getNumCols(),raster.getNumRows(),PConstants.RGB);	
		img.loadPixels();

		if (param == RasterMap.ELEV)
		{
			raster.getShadeImage(img.pixels, raster.getNumCols(),null);
		}
		else
		{
			// Calculate the requested parameter at 3x3 window size and display with standard shading
			GISFrame gisFrame = new SimpleGISFrame();
			gisFrame.addRaster(raster, GISFrame.PRIMARY);
			SurfParam surfParam = new SurfParam(gisFrame, param);
			surfParam.findParam();
			raster.getShadeImage(img.pixels, raster.getNumCols(),surfParam.getSurfParam());
		}
		img.updatePixels();

		// Restore original window size and shading settings.
		raster.setWSize(origWindowSize);
		raster.setpShade(origpShade);

		return img;
	}
	
	/** Creates a colour bar legend for the given raster.
	 *  @param g Graphics context in which to draw the legend.
	 *  @param raster Raster to be described by legend.
	 *  @param x Left hand edge of legend display area in pixel coordinates.
	 *  @param y Top edge of legend display area in pixel coordinates.
	 *  @param width Width of legend display area in pixel coordinates.
	 *  @param height Height of legend display area in pixel coordinates.
	 *  @param layout Layout direction.
	 */
	static void drawLegend(PGraphics g, RasterMap raster, float x, float y, float width, float height, LayoutType layout)
	{
		g.pushMatrix();
		g.pushStyle();
		
		if (layout == LayoutType.ROW)
		{
			// Draw horizontal legend.
			g.textAlign(PConstants.CENTER,PConstants.TOP);
			g.textSize(12*height/50);
			g.translate(-width/2, -height/2);
			for (float col=x; col<x+width-10; col++)
			{
				g.stroke(raster.getColourTable().findColour(PApplet.map(col, x, x+width-10, raster.getMinAttribute(), raster.getMaxAttribute())));
				g.line(col, y+height*.25f, col, y+height*0.75f);
			}

			g.fill(0);
			for (float col=x; col<x+width-10; col+=30)
			{
				g.text(PApplet.nfs(PApplet.map(col, x, x+width-10, raster.getMinAttribute(),raster.getMaxAttribute()),2,1),col,y+height*0.8f);
			}
		}
		else
		{
			// Draw vertical legend.
			g.textAlign(PConstants.RIGHT,PConstants.CENTER);
			g.textSize(12*width/75);
			g.translate(-width/2, -height/2);
			for (float row=y; row<y+height-10; row++)
			{
				g.stroke(raster.getColourTable().findColour(PApplet.map(row, y, y+height-10, raster.getMaxAttribute(), raster.getMinAttribute())));
				g.line(x+width/2, row, x+width, row);
			}

			g.fill(0);
			for (float row=y; row<=y+height; row+=30)
			{
				g.text(PApplet.nfs(PApplet.map(row, y, y+height-10, raster.getMaxAttribute(), raster.getMinAttribute()),2,1),x+width/2-2,row);

			}
		}
	
		
		g.popStyle();
		g.popMatrix();
	}
	
	/** Draws the given vector contours centred at the given location with the given width, height and line thickness. 
	 *  @param vectorMap Contours to draw.
	 *  @param cx Centre of display area in the x-direction in pixel units.
	 *  @param cy Centre of display area in the y-direction in pixel units.
	 *  @param width Width of display area in pixel units.
	 *  @param height Height of display area in pixel units.
	 *  @param graphics Graphics context in which to draw.
	 */
	static void drawContours(VectorMap vectorMap, float cx, float cy, float width, float height, float weightScale,PGraphics graphics)
	{
		float xOrigin = cx - width/2f;
		float yOrigin = cy - height/2f;
		
		@SuppressWarnings("rawtypes")
		List vectors = vectorMap.getGISVectors();

		Footprint bounds = vectorMap.getBounds();
		float minX = bounds.getXOrigin();
		float maxX = bounds.getXOrigin()+bounds.getMERWidth();
		float minY = bounds.getYOrigin();
		float maxY = bounds.getYOrigin()+bounds.getMERHeight();
		
		graphics.beginDraw();
		graphics.smooth();
		graphics.stroke(0,180);
		graphics.noFill();
		graphics.strokeWeight(weightScale);
		
		for (int i=0; i<vectors.size(); i++)
		{
			GISVector vector = (GISVector)vectors.get(i);
			float[] x = new float[vector.getNumCoords()];
			float[] y = new float[vector.getNumCoords()];
			
			vector.getCoords(x, y);

			if ((vector.getAttribute()*100)%25 == 0)
			{
				graphics.strokeWeight(weightScale*1.3f);
				graphics.stroke(0,120);
			}
			else
			{
				graphics.strokeWeight(weightScale);
				graphics.stroke(0,60);
			}

			graphics.beginShape();
			for (int j=0; j<x.length; j++)
			{
				graphics.vertex(PApplet.map(x[j],minX,maxX,xOrigin,xOrigin+width),PApplet.map(y[j],minY,maxY,yOrigin+height,yOrigin));
			}        	
			graphics.endShape();
		}
		graphics.endDraw();
	}


	/** Provides a set of rectangular boundaries for arranging a set of rectangles in a larger
	 *  rectangular space. This version ignores the aspect ratio of the features to be laid out. 
	 *  Can be used for displaying one or more rasters in a fixed space by arranging them into rows and columns. 
	 *  The precise layout is determined by the layout type and number of rectangles to be arranged.
	 *  @param xOrigin Left hand side position of display area in pixel units.
	 *  @param yOrigin Top position of display area in pixel units.
	 *  @param width Width of display area in pixel units.
	 *  @param height Height of display area in pixel units.
	 *  @param numRectangles Number of rectangular areas to fit in display area.
	 *  @param layout Type of layout desired (e.g. GRID, ROW or COLUMN).
	 *  @return Coordinates of the given number of rectangles in pixel units.
	 */
	static Rectangle2D.Float[] getBoundaries(float xOrigin, float yOrigin, float width, float height, int numRectangles, LayoutType layout)
	{
		return getBoundaries(xOrigin, yOrigin, width, height, numRectangles, 1, layout);
	}
	
	/** Provides a set of rectangular boundaries for arranging a set of rectangles in a larger
	 *  rectangular space. Can be used for displaying one or more rasters in a fixed space by
	 *  arranging them into rows and columns. The precise layout is determined by the layout type, 
	 *  the aspect ratio of the rectangles and number of them to be arranged.
	 *  @param xOrigin Left hand side position of display area in pixel units.
	 *  @param yOrigin Top position of display area in pixel units.
	 *  @param width Width of display area in pixel units.
	 *  @param height Height of display area in pixel units.
	 *  @param numRectangles Number of rectangular areas to fit in display area.
	 *  @param aspectRatio Width / height aspect ratio of the rectangles to lay out with values greater than 1 indicating 'landscape' and less than 1 indicating portrait.
	 *  @param layout Type of layout desired (e.g. GRID, ROW or COLUMN).
	 *  @return Coordinates of the given number of rectangles in pixel units.
	 */
	static Rectangle2D.Float[] getBoundaries(float xOrigin, float yOrigin, float width, float height, int numRectangles, float aspectRatio, LayoutType layout)
	{
		if (numRectangles < 1)
		{
			System.err.println("Warning: numRectangles should be at least 1 in getBoundaries()");
			return null;
		}

		int numRows,numCols;
		Rectangle2D.Float[] rects = new Rectangle2D.Float[numRectangles]; 

		if (layout == LayoutType.COLUMN)
		{
			numRows = 1;
			numCols = numRectangles;
		}
		else if (layout == LayoutType.ROW)
		{
			numRows = numRectangles;
			numCols = 1;
		}
		else	// Default to grid layout.
		{
			int longSide,shortSide;
			
			
			switch (numRectangles)
			{
				case 1:
				shortSide = 1;
				longSide = 1;
				break;
				
				case 2:
					shortSide = 1;
					longSide = 2;
					break;
	
				case 3:
				case 4:
					shortSide = 2;
					longSide = 2;
					break;
	
				case 5:
				case 6:
					shortSide = 2;
					longSide = 3;
					break;
	
				case 7:
				case 8:
					shortSide = 2;
					longSide = 4;
					break;
	
				case 9:
					shortSide = 3;
					longSide = 3;
					break;
	
				case 10:
					shortSide = 2;
					longSide = 5;
					break;
	
				case 11:
				case 12:
					shortSide = 3;
					longSide = 4;
					break;
	
				case 13:
				case 14:
				case 15:
					shortSide = 3;
					longSide = 5;
					break;
	
				case 16:
					shortSide = 4;
					longSide = 4;
					break;
	
				case 17:
				case 18:
				case 19:
				case 20:
					shortSide = 4;
					longSide = 5;
					break;
					
				case 21:
				case 22:
				case 23:
				case 24:
				case 25:
					shortSide = 5;
					longSide = 5;
					break;
					
				case 26:
				case 27:
				case 28:
				case 29:
				case 30:
					shortSide = 5;
					longSide = 6;
					break;
					
				case 31:
				case 32:
				case 33:
				case 34:
				case 35:
				case 36:
					shortSide = 6;
					longSide = 6;
					break;
					
				default:
					System.err.println("Cannot lay out more than 36 items.");
					shortSide = 10;
					longSide = 10;

			}	

			if (width >= height*aspectRatio)
			{
				// Landscape arrangement.
				numCols = longSide;
				numRows = shortSide;
			}
			else
			{
				// Portrait arrangement.
				numRows = longSide;
				numCols = shortSide;
			}
		}


		float x = xOrigin;
		float y = yOrigin;
		float dispWidth = width/numCols;
		float dispHeight = height/numRows;
		int col = 0;

		for (int i=0; i< numRectangles; i++)
		{
			rects[i] = new Rectangle2D.Float(x,y,dispWidth,dispHeight);

			// Find position of next rectangle if more than one.
			col++;
			x += dispWidth;
			if (col == numCols)
			{
				col = 0;
				x = xOrigin;
				y += dispHeight;
			}	
		}
		return rects;
	}


	/** Provides the suffix to convert a number such as 23 into 23rd, 24 into 24th etc.
	 *  @param value Numeric value onto which suffix is to be added.
	 *  @return The suffix to add.
	 */
	static String getOrdinalSuffix(int value)
	{
		int cleanValue = Math.abs(value);
		final int lastDigit     = cleanValue%10;
		final int lastTwoDigits = cleanValue%100;

		switch (lastDigit)
		{
		case 1 :
			return lastTwoDigits==11 ? "th":"st";

		case 2:
			return lastTwoDigits==12 ? "th":"nd";

		case 3:
			return lastTwoDigits==13 ? "th":"rd";

		default:
			return "th";
		}
	}

}
